﻿using System.Runtime.InteropServices;
using OnDash.Mathematics.Data;
using OnDash.Mathematics.Matrix;

namespace OnDash.Mathematics.Vector;

public struct Vector3(float x, float y, float z) : IEquatable<Vector3>
{
    public float X = x;

    public float Y = y;

    public float Z = z;

    public Vector3(float value) : this(value, value, value)
    {
    }

    public Vector3(Vector2 vector2) : this(vector2.X, vector2.Y, 0)
    {
    }

    public Vector3(Vector3 vector3) : this(vector3.X, vector3.Y, vector3.Z)
    {
    }

    public Vector2 Xy
    {
        get => new(X, Y);
        set
        {
            X = value.X;
            Y = value.Y;
        }
    }

    public Vector2 Xz
    {
        get => new(X, Z);
        set
        {
            X = value.X;
            Z = value.Y;
        }
    }

    public Vector2 Yx
    {
        get => new(Y, X);
        set
        {
            Y = value.X;
            X = value.Y;
        }
    }

    public Vector2 Yz
    {
        get => new(Y, Z);
        set
        {
            Y = value.X;
            Z = value.Y;
        }
    }

    public Vector2 Zx
    {
        get => new(Z, X);
        set
        {
            Z = value.X;
            X = value.Y;
        }
    }

    public Vector2 Zy
    {
        get => new(Z, Y);
        set
        {
            Z = value.X;
            Y = value.Y;
        }
    }

    public Vector3 Xyz
    {
        get => new(X, Y, Z);
        set
        {
            X = value.X;
            Y = value.Y;
            Z = value.Z;
        }
    }

    public Vector3 Xzy
    {
        get => new(X, Z, Y);
        set
        {
            X = value.X;
            Z = value.Y;
            Y = value.Z;
        }
    }

    public Vector3 Yxz
    {
        get => new(Y, Z, X);
        set
        {
            Y = value.X;
            Z = value.Y;
            X = value.Z;
        }
    }

    public Vector3 Yzx
    {
        get => new(Y, Z, X);
        set
        {
            Y = value.X;
            Z = value.Y;
            X = value.Z;
        }
    }

    public Vector3 Zxy
    {
        get => new(Z, X, Y);
        set
        {
            Z = value.X;
            X = value.Y;
            Y = value.Z;
        }
    }

    public Vector3 Zyx
    {
        get => new(Z, Y, X);
        set
        {
            Z = value.X;
            Y = value.Y;
            X = value.Z;
        }
    }

    public float this[int index]
    {
        get
        {
            return index switch
            {
                0 => X,
                1 => Y,
                2 => Z,
                _ => throw new IndexOutOfRangeException($"{index} is out of range.")
            };
        }
        set
        {
            switch (index)
            {
                case 0:
                    X = value;
                    break;
                case 1:
                    Y = value;
                    break;
                case 2:
                    Z = value;
                    break;
                default:
                    throw new IndexOutOfRangeException($"{index} is out of range.");
            }
        }
    }

    public float Length => MathHelper.Sqrt(X * X + Y * Y + Z * Z);

    public float LengthSquared => X * X + Y * Y + Z * Z;

    public Vector3 Normalized()
    {
        var v = this;
        v.Normalize();
        return v;
    }

    public void Normalize()
    {
        var scale = MathHelper.InverseSqrtFast(LengthSquared);
        X *= scale;
        Y *= scale;
        Z *= scale;
    }

    public static readonly Vector3 UnitX = new(1, 0, 0);
    public static readonly Vector3 UnitY = new(0, 1, 0);
    public static readonly Vector3 UnitZ = new(0, 0, 1);

    public static readonly Vector3 Zero = new(0, 0, 0);
    public static readonly Vector3 One = new(1, 1, 1);

    public static readonly int SizeInBytes = Marshal.SizeOf(new Vector3());

    public static Vector3 Add(Vector3 left, Vector3 right)
        => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);

    public static Vector3 Subtract(Vector3 left, Vector3 right)
        => new(left.X - right.X, left.Y - right.Y, left.Z - right.Z);

    public static Vector3 Multiply(Vector3 left, float right)
        => new(left.X * right, left.Y * right, left.Z * right);

    public static Vector3 Multiply(Vector3 left, Vector3 right)
        => new(left.X * right.X, left.Y * right.Y, left.Z * right.Z);

    public static Vector3 Divide(Vector3 left, float right)
        => new(left.X / right, left.Y / right, left.Z / right);

    public static Vector3 Divide(Vector3 left, Vector3 right)
        => new(left.X / right.X, left.Y / right.Y, left.Z / right.Z);

    public static Vector3 Min(Vector3 left, Vector3 right)
        => left.LengthSquared < right.LengthSquared ? left : right;

    public static Vector3 Max(Vector3 left, Vector3 right)
        => left.LengthSquared >= right.LengthSquared ? left : right;

    public static Vector3 ComponentMin(Vector3 left, Vector3 right)
    {
        return new Vector3(
            left.X < right.X ? left.X : right.X,
            left.Y < right.Y ? left.Y : right.Y,
            left.Z < right.Z ? left.Z : right.Z
        );
    }

    public static Vector3 ComponentMax(Vector3 left, Vector3 right)
    {
        return new Vector3(
            left.X > right.X ? left.X : right.X,
            left.Y > right.Y ? left.Y : right.Y,
            left.Z > right.Z ? left.Z : right.Z
        );
    }

    public static Vector3 MagnitudeMin(Vector3 left, Vector3 right)
        => left.LengthSquared < right.LengthSquared ? left : right;

    public static Vector3 MagnitudeMax(Vector3 left, Vector3 right)
        => left.LengthSquared >= right.LengthSquared ? left : right;

    public static Vector3 Clamp(Vector3 value, Vector3 min, Vector3 max)
    {
        return new Vector3(
            value.X < min.X ? min.X : value.X > max.X ? max.X : value.X,
            value.Y < min.Y ? min.Y : value.Y > max.Y ? max.Y : value.Y,
            value.Z < min.Z ? min.Z : value.Z > max.Z ? max.Z : value.Z
        );
    }

    public static float Distance(Vector3 value1, Vector3 value2)
        => MathHelper.Sqrt(DistanceSquared(value1, value2));

    public static float DistanceSquared(Vector3 value1, Vector3 value2)
        => (value2.X - value1.X) * (value2.X - value1.X) + (value2.Y - value1.Y) * (value2.Y - value1.Y);

    public static Vector3 Normalize(Vector3 value)
        => new(value.X / value.Length, value.Y / value.Length, value.Z / value.Length);

    public static Vector3 NormalizeFast(Vector3 value)
    {
        return new Vector3(
            value.X * MathHelper.InverseSqrtFast(value.X * value.X + value.Y * value.Y + value.Z * value.Z),
            value.Y * MathHelper.InverseSqrtFast(value.X * value.X + value.Y * value.Y + value.Z * value.Z),
            value.Z * MathHelper.InverseSqrtFast(value.X * value.X + value.Y * value.Y + value.Z * value.Z)
        );
    }

    public static float Dot(Vector3 left, Vector3 right)
        => left.X * right.X + left.Y * right.Y + left.Z * right.Z;

    public static Vector3 Cross(Vector3 left, Vector3 right)
    {
        return new Vector3(
            left.Y * right.Z - left.Z * right.Y,
            left.Z * right.X - left.X * right.Z,
            left.X * right.Y - left.Y * right.X
        );
    }

    public static Vector3 Lerp(Vector3 start, Vector3 end, float amount)
    {
        return new Vector3(
            amount * (end.X - start.X) + start.X,
            amount * (end.Y - start.Y) + start.Y,
            amount * (end.Z - start.Z) + start.Z
        );
    }

    public static Vector3 Negate(Vector3 value)
        => new(-value.X, -value.Y, -value.Z);

    public static Vector3 BaryCentric(Vector3 a, Vector3 b, Vector3 c, float u, float v)
        => a + u * (b - a) + v * (c - a);

    public static Vector3 TransformVector(Vector3 position, Matrix4 matrix)
    {
        return new Vector3(
            position.X * matrix.M11 + position.Y * matrix.M21 + position.Z * matrix.M31 + matrix.M41,
            position.X * matrix.M12 + position.Y * matrix.M22 + position.Z * matrix.M32 + matrix.M42,
            position.X * matrix.M13 + position.Y * matrix.M23 + position.Z * matrix.M33 + matrix.M43
        );
    }

    public static Vector3 TransformNormal(Vector3 normal, Matrix4 matrix)
    {
        var inverse = Matrix4.Invert(matrix);
        return TransformNormalInverse(normal, inverse);
    }

    public static Vector3 TransformNormalInverse(Vector3 normal, Matrix4 matrix)
    {
        return new Vector3(
            normal.X * matrix.M11 + normal.Y * matrix.M21 + normal.Z * matrix.M31,
            normal.X * matrix.M12 + normal.Y * matrix.M22 + normal.Z * matrix.M32,
            normal.X * matrix.M13 + normal.Y * matrix.M23 + normal.Z * matrix.M33
        );
    }

    public static Vector3 TransformPosition(Vector3 position, Matrix4 matrix)
    {
        return new Vector3(
            position.X * matrix.M11 + position.Y * matrix.M21 + position.Z * matrix.M31 + matrix.M41,
            position.X * matrix.M12 + position.Y * matrix.M22 + position.Z * matrix.M32 + matrix.M42,
            position.X * matrix.M13 + position.Y * matrix.M23 + position.Z * matrix.M33 + matrix.M43
        );
    }

    public static Vector3 Transform(Vector3 position, Matrix3 matrix)
    {
        return new Vector3(
            position.X * matrix.M11 + position.Y * matrix.M21 + position.Z * matrix.M31,
            position.X * matrix.M12 + position.Y * matrix.M22 + position.Z * matrix.M32,
            position.X * matrix.M13 + position.Y * matrix.M23 + position.Z * matrix.M33
        );
    }

    public static Vector3 Transform(Vector3 position, Quaternion quaternion)
    {
        var xyz = quaternion.Xyz;

        var temp = Cross(xyz, position);
        var temp2 = Multiply(position, quaternion.W);
        temp = Add(temp, temp2);
        temp = Cross(xyz, temp);
        temp = Multiply(temp, 2);
        return Add(position, temp);
    }

    public static Vector3 Transform(Matrix3 matrix, Vector3 position)
    {
        return new Vector3(
            position.X * matrix.M11 + position.Y * matrix.M21 + position.Z * matrix.M31,
            position.X * matrix.M12 + position.Y * matrix.M22 + position.Z * matrix.M32,
            position.X * matrix.M13 + position.Y * matrix.M23 + position.Z * matrix.M33
        );
    }

    public static Vector3 TransformPerspective(Vector3 position, Matrix4 matrix)
    {
        var v = new Vector4(position.X, position.Y, position.Z, 0);
        Vector4.Transform(v, matrix);
        return new Vector3(v.X / v.W, v.Y / v.W, v.Z / v.W);
    }

    public static float CalculateAngle(Vector3 first, Vector3 second)
    {
        var temp = Dot(first, second);
        return MathHelper.Acos(MathHelper.Clamp(temp / (first.Length * second.Length), -1, 1));
    }

    public static Vector3 Project(Vector3 vector, float x, float y, float width, float height, float minZ, float maxZ,
        Matrix4 worldViewProjection)
    {
        var result = Vector4.Transform(new Vector4(vector.X, vector.Y, vector.Z, 1), worldViewProjection);

        result /= result.W;

        return new Vector3
        (
            x + width * ((result.X + 1) * 0.5f),
            y + height * ((result.Y + 1) * 0.5f),
            minZ + (maxZ - minZ) * ((result.Z + 1) * 0.5f)
        );
    }

    public static Vector3 Unproject(Vector3 vector, float x, float y, float width, float height, float minZ, float maxZ,
        Matrix4 inverseWorldViewProjection)
    {
        var result = new Vector4
        (
            (vector.X - x) / width * 2 - 1,
            (vector.Y - y) / height * 2 - 1,
            (vector.Z - minZ) / (maxZ - minZ) * 2 - 1,
            1
        );

        result = Vector4.Transform(result, inverseWorldViewProjection);

        switch (result.W)
        {
            case > 0:
                result.X /= result.W;
                result.Y /= result.W;
                result.Z /= result.W;
                break;
        }

        return new Vector3(result.X, result.Y, result.Z);
    }

    public static Vector3 operator +(Vector3 left, Vector3 right) => Add(left, right);
    public static Vector3 operator -(Vector3 left, Vector3 right) => Subtract(left, right);
    public static Vector3 operator -(Vector3 vector) => Negate(vector);
    public static Vector3 operator *(Vector3 left, float right) => Multiply(left, right);
    public static Vector3 operator *(float left, Vector3 right) => Multiply(right, left);
    public static Vector3 operator *(Vector3 left, Vector3 right) => Multiply(left, right);
    public static Vector3 operator *(Vector3 vector, Matrix3 matrix) => Transform(vector, matrix);
    public static Vector3 operator *(Matrix3 matrix, Vector3 vector) => Transform(matrix, vector);
    public static Vector3 operator *(Quaternion quaternion, Vector3 vector) => Transform(vector, quaternion);
    public static Vector3 operator /(Vector3 left, float right) => Divide(left, right);
    public static Vector3 operator /(Vector3 vector, Vector3 right) => Divide(vector, right);

    public static bool operator ==(Vector3 left, Vector3 right) => Equals(left, right);
    public static bool operator !=(Vector3 left, Vector3 right) => !Equals(left, right);

    public override string ToString()
        => string.Format("({0}{3}, {1}{3}, {2})", X, Y, Z, MathHelper.ListSeparator);

    public override int GetHashCode() => HashCode.Combine(X, Y, Z);

    public override bool Equals(object? obj) => obj is Vector3 vector && Equals(vector);

    public bool Equals(Vector3 other)
        => X.Equals(other.X) &&
           Y.Equals(other.Y) &&
           Z.Equals(other.Z);
}